package cmm;

//import java.io.Serializable;
//import java.net.InetAddress;


import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;

import server.SAIncramentalTaskContext;
import server.SATaskContext;
import server.ServerApplication;
import spread.*;

public class ClusterManagementModule implements IClusterManagementModule {

	private Thread CMMThread;
	
	private TreeMap<Integer, TaskEntry> TaskDB;
	
	private TreeMap<Integer, MachineEntry> MachineDB;
	
	private ProxyServer Proxy;
	
	private Integer ProxyMachineID = null;
	
	private SpreadConnection ClusterConnection;
	
	private SpreadGroup ClusterGroup;
	
//	private String privateName;
	
	private String GroupName;
	
//	private ConnectionInformation SAConnInfo;
	
	private Integer MachineDBLocalID;
	
	private ServerApplication SA;
	
	private AdvancedSpreadListener SpreadListener;
	
//	private String ClusterMachineName = null;
	
	private Thread ConstructorThread;
	
	private LinkedList<ToBeMentoredEntry> ToBeMentoredList;
	
	private LinkedList<Mentor> MentorList;
	
//	private MachineState LocalState;
	
	private MachineEntry LocalMachineEntry;
	
	private MessageQueue TrainingMessageQueue;
	
	private boolean InteractiveTrainingMode = true;
	
//------------------------------------------------------------------------
//  Private Methods	
//------------------------------------------------------------------------
	
	/**
	 * The method sends messages to the whole cluster via the 
	 * ClusterConnection. All cluster communication should be done 
	 * through this method. 
	 * 
	 * @param ClusterMSG
	 * @throws CMMExn_ClusterErr 
	 */
	private void SendClusterMessage(ClusterMessage ClusterMSG) throws CMMExn_ClusterErr{
		
		SA.AddLogEntry("CMM", "Info - Sending a message to the cluster. Message Type - " + ClusterMSG.getType());
		
		// Set this machine to be the sender machine 
		ClusterMSG.setSenderID(MachineDBLocalID);
		
		try {
			// Create a new SpreadMessage
			SpreadMessage Message = new SpreadMessage();
			Message.setObject(ClusterMSG);
			Message.addGroup(ClusterGroup);
			Message.setReliable();
			
			// Send the message to the cluster
			ClusterConnection.multicast(Message);
		} catch (SpreadException e) {
			// Report failure to send ClusterMessage
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to send a message to the cluster." + 
					" MessageType : " + ClusterMSG.getType());
			
			// Throw a Cluster Exception
			CMMExn_ClusterErr exn = new CMMExn_ClusterErr();
			throw exn;
		}
	}

//	------------------------------------------------------------------------	
	
	private void ProxyNewTask(ConnectionInformation ProxyConnInfo) throws CMMExn_ClusterErr {

		SA.AddLogEntry("CMM", "Info - Sending a \"Proxy New Task\" request to the Cluster.");
		
		// Create a Proxy Context to send
		ProxyContext Context = new ProxyContext();
		Context.putServerID(MachineDBLocalID);
		Context.putConnInfo(ProxyConnInfo);
		
		// Create a "New Task" message 
		ClusterMessage_ProxyNewTask msg = new ClusterMessage_ProxyNewTask(); 
		msg.setProxyContext(Context);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"New Task\" message to the cluster.");
			throw e;
		}
	}
	
//	------------------------------------------------------------------------	
	
	private Integer InsertNewTaskIntoDB(Integer MachineID, TaskContext Context) throws CMMExn_TaskErr{
		
		// Verify that the machine ID exists in the machine database
		if(!MachineDB.containsKey(MachineID))
		{
			SA.AddLogEntry("CMM", "Error - MachineID " + MachineID + " does not exist in the Machine Database.");
			CMMExn_TaskErr exn = new CMMExn_TaskErr();
			throw exn;
		}
	
		// Retrieve previous maximum task ID
		Integer MaxTaskID;
		if(TaskDB.isEmpty())
			MaxTaskID = new Integer(-1);
		else
			MaxTaskID = TaskDB.lastKey();
	
		// Create a new task ID
		// New task ID is prev task ID incramented by 1
		Integer NewTaskID = new Integer(MaxTaskID.intValue()+1);
	
		// Create a new task entry
		TaskEntry NewEntry = new TaskEntry(MachineID, Context);
	
		// Add the new task entry to the task database
		TaskDB.put(NewTaskID, NewEntry);
	
		// Update the task amount entry of the relevant machine in
		// the machine database
		// Retrieve prev machine entry
		MachineEntry MEntry = MachineDB.get(MachineID);
		// Incrament the task count 
		MEntry.incTaskCount();
		
		return NewTaskID;
	}

//------------------------------------------------------------------------

	private LinkedList<InetAddress> ParseSpreadConfig(String ConfigPath) throws FileNotFoundException, CMMExn_ClusterErr {
	
		LinkedList<InetAddress> MachineIPs = new LinkedList<InetAddress>();
	
		// Open config file
		BufferedReader Config = new BufferedReader(new FileReader(ConfigPath));
	
		// This indicates when the parser grts into the members section in the config file
		boolean MembersSection = false;
		String line = null;
		while(true) {
			try {
				// Read a line from the config file
				line = Config.readLine();
			} catch (IOException e) {
				// TODO remove stack print
				e.printStackTrace();
				SA.AddLogEntry("CMM", "Error - Failed to read from the spread.conf file.");
				try {
					Config.close();
				} catch (IOException e1) {
					// TODO remove stack print
					e1.printStackTrace();
				}
				throw new CMMExn_ClusterErr();
			}
			// Check if reacked the End Of File
			if(line == null){
				SA.AddLogEntry("CMM", "Error - spread.conf is wronglly formatted.");
				throw new CMMExn_ClusterErr();
			}
		
			// if got to the start of the members section 
			if(line.startsWith("Spread_Segment")){
				MembersSection = true;
			}
			// if not the first line of the members section
			else if(MembersSection){
				// if the members section has ended
				if(line.equals("}")){
					MembersSection = false;
					break;
				}
				// If in the middle of the members section
				else{
					// split the machine name and the machine ip
					// they are seperated by tabs 
					String[] words = line.split("\t");
					if(words.length < 3) {
						SA.AddLogEntry("CMM", "Error - spread.conf is wronglly formatted.");
						throw new CMMExn_ClusterErr();
					}
					InetAddress Addr = null;
					try {
						Addr = InetAddress.getByName(words[2]);
					} catch (UnknownHostException e) {
						// TODO remove stack print
						e.printStackTrace();
						SA.AddLogEntry("CMM", "Error - Machine address " + words[2] + " in spread.conf could not be recognized.");
						throw new CMMExn_ClusterErr();
					}
					// Add the machine's IP to the machines' IP list
					MachineIPs.add(Addr);
					InetAddress MyAddr = null;
					try {
						MyAddr = InetAddress.getLocalHost();
					} catch (UnknownHostException e) {
						// TODO remove stack print
						e.printStackTrace();
						SA.AddLogEntry("CMM", "Error - Local IP address could not be retrieved.");
						throw new CMMExn_ClusterErr();
					}
					// If this is the local IP
					if(MyAddr.equals(Addr))
						// Store the machine name
						LocalMachineEntry.putClusterMachineName(words[1]);
				}
			}
		}
	
		return MachineIPs;
	}

//------------------------------------------------------------------------

	private void ProcessTaskQueue() {
		// TODO Implement
		while(!TrainingMessageQueue.isEmpty()){
			ClusterMessage ClusterMSG = TrainingMessageQueue.Pop();
			switch(ClusterMSG.getType()){
			// Application Messages
				case APP_NEW_TASK :
				{
					// Convert the message to a "Application New Task" message object
					ClusterMessage_AppNewTask msg = (ClusterMessage_AppNewTask)ClusterMSG;
					// Inform the CMM of a "Application New Task" message
					ReceiveAppNewTask(msg.getSenderID(), msg.getTaskContext());
					break;
				}
				case APP_UPDATE_TASK :
				{
					// Convert the message to a "Application Update Task" message object
					ClusterMessage_AppUpdateTask msg = (ClusterMessage_AppUpdateTask)ClusterMSG;
					// Inform the CMM of an "Application Update Task" message
					ReceiveAppUpdateTask(msg.getSenderID(), msg.getTaskID(), msg.getContextUpdate());
					break;
				}
				case APP_TERMINATE_TASK :
				{
					// Convert the message to a "Application Terminate Task" message object
					ClusterMessage_AppTerminateTask msg = (ClusterMessage_AppTerminateTask)ClusterMSG;
					// Inform the CMM of an "Application Terminate Task" message
					ReceiveAppTerminateTask(msg.getSenderID(), msg.getTaskID());
					break;
				}
				// Proxy Messages
				case PROXY_NEW_TASK :
				{
					// Convert the message to a "Proxy New Task" message object
					ClusterMessage_ProxyNewTask msg = (ClusterMessage_ProxyNewTask)ClusterMSG;
					// Inform the CMM of a "Proxy New Task" message
					ReceiveProxyNewTask(msg.getSenderID(), msg.getProxyContext());
					break;
				}
				// CMM Messages
				// note: this is not a real message and it is not recieved from the cluster.
				// it may only come from the Training Message Queue.
				case REMOVE_MACHINE :
				{
					// Convert the message to a "Remove Machine" message object
					ClusterMessage_RemoveMacine msg = (ClusterMessage_RemoveMacine)ClusterMSG;
					// Remove the leaving machine from the Machine Database
					RemoveMachineFromCluster(msg.getSenderID());
					break;
					
				}
				case NEW_SERVER_READY :
				{
					// Convert the message to a "New Server Ready" message object
					ClusterMessage_NewServerReady msg = (ClusterMessage_NewServerReady)ClusterMSG;
					// Inform the CMM of a "New Machine Ready" message
					ReceiveNewServerReady(msg.getNewMachineEntry());
					break;
				}
				default :
					// Inform the CMM that an unsupported message was recieved
					ReceiveUnsupportedMessage();
			}
		}
	}

//	------------------------------------------------------------------------	
	
	private void WaitForEnter(String MessageToUser) {

			System.out.println(MessageToUser);
			BufferedReader userIn =
		        new BufferedReader(
		        new InputStreamReader( System.in ) );
			
			try {
				userIn.readLine();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
		}
	}
	
//	------------------------------------------------------------------------	
	
	private synchronized void RemoveMachineFromCluster(Integer MachineID) {
		
		MachineDB.remove(MachineID);
		SA.AddLogEntry("CMM", "Info - Removed Machine with MachineID " + MachineID + " from the Machine Database.");
	}
	
//------------------------------------------------------------------------
//  Public Methods	
//------------------------------------------------------------------------
		
	
	/**
	 * The constructor of the Cluster Management Module. It initializes all
	 * the communication with the cluster and returns once the machine is
	 * synchronized with all the active machines in the cluster - 
	 * hence after training.
	 * 
	 * @param SA - A reference to the Server Application which activates the CMM 
	 * @param mySAInfo - The connection information of the SA for it's clients 
	 * @param ProxyConnInfo - The connection information for the Proxy Server
	 * @param ClusterGroupName - The Cluster group name for the CMM to connect to
	 * @param ConfigPath - The path of the "spread.conf" file (including the filename) 
	 * @throws CMMExn_ClusterErr - An error occured while connecting to the Cluster
	 * @throws FileNotFoundException - "spread.conf" file not found
	 * @throws UnknownHostException 
	 */
	public ClusterManagementModule(ServerApplication SA, ConnectionInformation mySAInfo, ConnectionInformation ProxyConnInfo, String ClusterGroupName, String ConfigPath) throws CMMExn_ClusterErr, FileNotFoundException, UnknownHostException {
		
		this.SA = SA;
//		// TODO CHECK if need to make unique
//		privateName = "CMMServer";
		GroupName = ClusterGroupName;
		
		LocalMachineEntry = new MachineEntry();
		LocalMachineEntry.putConnInfo(mySAInfo);
		LocalMachineEntry.putMachineState(MachineState.PRE_TRAINING);
		LocalMachineEntry.putTaskCount(0);
		try {
			LocalMachineEntry.putClusterMachineName(InetAddress.getLocalHost().getHostName());
		} catch (UnknownHostException e2) {
			// TODO Auto-generated catch block
			e2.printStackTrace();
			throw e2;
		}
		// note: LocalMachineEntry.ClusterMachineName is set in the "spread.conf" parser
		
		// Initialize the task database
		TaskDB = new TreeMap<Integer, TaskEntry>();
		
		// Initialize the machine database
		MachineDB = new TreeMap<Integer, MachineEntry>();
		
		// Initialize the Snapshot TaskDB List
		ToBeMentoredList = new LinkedList<ToBeMentoredEntry>();
		
		MentorList = new LinkedList<Mentor>();
		
		// Store refference to the current thread
		ConstructorThread = Thread.currentThread();
		
		SA.AddLogEntry("CMM", "Info - CMM Started.");
		SA.AddLogEntry("CMM", "Info - Trying to connect to the cluster via Spread");
		
		// Start Spread connection
		boolean priority = false;
		boolean groupMembership = true;
		boolean connected = false;
		
		ClusterConnection = new SpreadConnection();
//		LinkedList<InetAddress> CanditateIPs = null;
//		try{
//			CanditateIPs = ParseSpreadConfig(ConfigPath);
//		} catch (CMMExn_ClusterErr e){
//			// TODO remove stack print
//			e.printStackTrace();
//			SA.AddLogEntry("CMM", "Error - Error in processing spread.conf.");
//			throw e;
//		}
		
		// private connection name is set to the name of the local machine
		String privateName = LocalMachineEntry.getClusterMachineName();
		
//		while(!CanditateIPs.isEmpty()){
//			InetAddress Addr = CanditateIPs.removeFirst();
//			try {
//				if(!Addr.equals(InetAddress.getLocalHost())){
//					try {
//						SA.AddLogEntry("CMM", "Info - Trying to establish connection with Spread deamon at " + Addr);
//						// Open a new connection to the Spread deamon in the machine of Addr
//						ClusterConnection.connect(Addr, 0, privateName, priority, groupMembership);
//						connected = true;
//						break;
//					} catch (SpreadException e) {
//						SA.AddLogEntry("CMM", "Info - Failed to establish Spread connection with " + Addr);
//					}
//				}
//			} catch (UnknownHostException e) {
//				// TODO remove stack print
//				e.printStackTrace();
//				SA.AddLogEntry("CMM", "Error - Failed to accuire local IP address.");
//				throw new CMMExn_ClusterErr();
//			}
//		}
//		if(!connected){
		try {
			SA.AddLogEntry("CMM", "Info - Attempting to connect local Spread Deamon.");
			ClusterConnection.connect(null, 0, privateName, priority, groupMembership);
		} catch (SpreadException e) {
			// TODO Remove stack print
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to establish Spread connection with deamon on local machine.");
			throw new CMMExn_ClusterErr();
		}
//		}
		try {	
			// Create and add a new Spread listener for the spread
			SpreadListener = new AdvancedSpreadListener(this, LocalMachineEntry.getClusterMachineName(), LocalMachineEntry.getMachineState());
			ClusterConnection.add(SpreadListener);
			
			// Join the server group in the cluster
			ClusterGroup = new SpreadGroup();
			ClusterGroup.join(ClusterConnection, GroupName);
			
			
		} catch (SpreadException e) {
			// TODO Remove StackPrint
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to connect to the cluster. Not running CMM thread.");
			CMMExn_ClusterErr exn = new CMMExn_ClusterErr();
			throw exn;
		}
		
		SA.AddLogEntry("CMM", "Info - Spread connection to the cluster established.");
		
		// Sleep until training is over
		long SleepTime = 1000;
		while(true){
			try {
				Thread.sleep(SleepTime);
			} catch (InterruptedException e) {
				// if sleep interupted - training ended
				break;
			}
		}		
		
		// The CMM is now ready for steady state activity

		if(InteractiveTrainingMode)
			WaitForEnter("Press Enter to send \"New Server Ready\" message to the Cluster.");
		
		// Send message to the cluster - "New Server Ready"
		ClusterMessage_NewServerReady msg = new ClusterMessage_NewServerReady();
		// Put local machine details into the message
		LocalMachineEntry.putMachineState(MachineState.ACTIVE);
		SpreadListener.setLocalMachineState(LocalMachineEntry.getMachineState());
		msg.setNewMachineEntry(LocalMachineEntry);
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"New Server Ready\" message to the Cluster.");
			throw e;
		}
		
		// Sleep until message comes back and local MachineID established
		while(true){
			try {
				Thread.sleep(SleepTime);
			} catch (InterruptedException e) {
				// if sleep interupted - training ended
				break;
			}
		}
		
		// -- Proxy Initialization --
		// TODO Replace with proxy selection and parameter transfer
		if(MachineDBLocalID.equals(new Integer(0))){
			try {
				Proxy = new ProxyServer(this, ProxyConnInfo);
				SA.AddLogEntry("CMM", "Info - Proxy Server initialized.");
			} catch (IOException e1) {
				// TODO remove StackPrint
				e1.printStackTrace();
				SA.AddLogEntry("CMM", "Error - Failed to initialize the Proxy Server.");
				Close();
				CMMExn_ClusterErr exn = new CMMExn_ClusterErr();
				throw exn;
			}
		
			try{
				// Send "Proxy New Task" message to the Cluster
				ProxyNewTask(ProxyConnInfo);
			} catch (CMMExn_ClusterErr e){
				SA.AddLogEntry("CMM", "Error - Failed to send \"Proxy New Task\" message to the Cluster.");
				try {
					// TODO Add Proxy Check
					Proxy.Close();
					SA.AddLogEntry("CMM", "Info - Terminated Proxy Server.");
				} catch (IOException e1) {
					// TODO remove StackTrace
					e1.printStackTrace();
				}
				throw e;
			}
		}
		
		// Create a new thread for the CMM
		// TODO implement a load balancing thread
//		CMMThread = new Thread(new ClusterManagementModule(SA, myInfo));
//		CMMThread.run();
		
		// TODO Continue...
	}

//------------------------------------------------------------------------	
	
	/* 
	 * This is the code of the CMM thread, which runs in a 
	 * different context.
	 * 
	 * (non-Javadoc)
	 * @see server.IClusterManagementModule#run()
	 */
	public void run(){
		// TODO Write CMM.run()

		// SA.AddLogEntry("CMM", "Info - Started CMM thread.");
				
		// TODO Add steady state activity here
		// Here Be Dragons ...ok - maybe just load balancing and fault tolerance
	}

//	------------------------------------------------------------------------
//  ProxyServer Interface	
//	------------------------------------------------------------------------	
	
	public synchronized ConnectionInformation FindLeastBusyServer() {

		SA.AddLogEntry("CMM", "Info - Received a \"Find Least Busy Server\" request from the Proxy server.");
		
		// Transform the machine database from a TraaMap into a Set
		Set<Map.Entry<Integer, MachineEntry>> MachineSet = MachineDB.entrySet();
		// Create an Iterator to run through the Set
		Iterator<Map.Entry<Integer, MachineEntry>> it = MachineSet.iterator();
		int MinTaskAmount = Integer.MAX_VALUE;
		ConnectionInformation ConnInfo = null;
		Integer SelectedMachineID = null;
		// Run througn the Set
		while(it.hasNext()){
			// Retrieve an entry from the Set
			Map.Entry<Integer, MachineEntry> MapEntry = it.next();
			// Retrieve the machine entry from the Set entry
			MachineEntry MEntry = MapEntry.getValue();
			// if the machine's task amount is the current minimum
			if(MEntry.getTaskCount() <= MinTaskAmount){
				// Set task count minimum to this machine's task count 
				MinTaskAmount = MEntry.getTaskCount();
				// Take this machine's connection information for the return value
				ConnInfo = MEntry.getConnInfo();
				// Set MachineID 
				SelectedMachineID = MapEntry.getKey();
			}
		}
		
		SA.AddLogEntry("CMM", "Info - Chose least busy server - MachineID " + SelectedMachineID);
		
		return ConnInfo;
	}

//	------------------------------------------------------------------------	
	
	public void AddLogEntry(String ModuleName, String Entry) {

		SA.AddLogEntry(ModuleName, Entry);
	}

//	------------------------------------------------------------------------
//  Mentor Interface	
//	------------------------------------------------------------------------	
	
	public void SendTrainingMsg(ClusterMessage_TrainingUpdateTask Message) throws CMMExn_ClusterErr {
		
		try {
			SendClusterMessage(Message);
		} catch (CMMExn_ClusterErr e) {
			// TODO remove stack print
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to send a training message to the cluster.");
			SA.UnrecoverableClusterError();
			throw e;
		}
	}
	
//	------------------------------------------------------------------------	
	
	public void SendTrainingMsg(ClusterMessage_TrainingTerminateTask Message) throws CMMExn_ClusterErr {
		
		try {
			SendClusterMessage(Message);
		} catch (CMMExn_ClusterErr e) {
			// TODO remove stack print
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to send a training termination message to the cluster.");
			SA.UnrecoverableClusterError();
			throw e;
		}
	}
	
//	------------------------------------------------------------------------
//  ServerApplication Interface	
//	------------------------------------------------------------------------	

	public void AppNewTask(SATaskContext Context) throws CMMExn_ClusterErr {

		SA.AddLogEntry("CMM", "Info - Recieved a \"New Task\" request from the SA.");
		
		// Create a "New Task" message 
		ClusterMessage_AppNewTask msg = new ClusterMessage_AppNewTask();
		msg.setTaskContext(Context);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"New Task\" message to the cluster.");
			throw e;
		}
	}

//------------------------------------------------------------------------	
	
	public void AppTaskContextUpdate(Integer TaskID,
			SAIncramentalTaskContext ContextChange) throws CMMExn_TaskErr, CMMExn_ClusterErr {

		SA.AddLogEntry("CMM", "Info - Recieved a \"Task Context Update\" request from the SA for TaskID " + TaskID);
		
		// This past is synchronized because 
		// access to CMM databases is restricted 
		synchronized(this){
			// Check if task exists in task database
			if(!TaskDB.containsKey(TaskID)){
				SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
		
			// Check if task belongs to local machine
			TaskEntry Task = TaskDB.get(TaskID);
			if(!Task.getAssignedServerID().equals(MachineDBLocalID)){
				SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + Task.getAssignedServerID() 
						+ ") does not match local MachineID (" + MachineDBLocalID +")");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
			
			// Check if task type is APPLICATION
			if(Task.getContext().getType()!=TaskType.APPLICATION){
				SA.AddLogEntry("CMM", "Error - Task " + TaskID + " is not of APPLICATION type.");
				// Application is not allowed to update non-application tasks.
				throw new CMMExn_TaskErr();
			}
		}
		
		// Create an "Update Task" message
		ClusterMessage_AppUpdateTask msg = new ClusterMessage_AppUpdateTask();
		msg.setTaskID(TaskID);
		msg.setContextUpdate(ContextChange);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"Update Task\" message to the cluster." +
					" TaskID : " + TaskID);
			throw e;
		}
	}

//------------------------------------------------------------------------	
	
	public void AppTaskFinish(Integer TaskID) throws CMMExn_TaskErr, CMMExn_ClusterErr {
		
		SA.AddLogEntry("CMM", "Info - Recieved a \"Task Finish\" request for TaskID " + TaskID);
		
		// This past is synchronized because 
		// access to CMM databases is restricted 
		synchronized(this){
			// Check if task exists in task database
			if(!TaskDB.containsKey(TaskID)){
				SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
		
			// Check if task belongs to local machine
			TaskEntry Task = TaskDB.get(TaskID);
			if(!Task.getAssignedServerID().equals(MachineDBLocalID)){
				SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + Task.getAssignedServerID() 
					+ ") does not match local MachineID (" + MachineDBLocalID +")");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
				
			// Check if task type is APPLICATION
			if(Task.getContext().getType()!=TaskType.APPLICATION){
				SA.AddLogEntry("CMM", "Error - Task " + TaskID + " is not of APPLICATION type.");
				// Application is not allowed to terminate non-application tasks.
				throw new CMMExn_TaskErr();
			}
		}
		
		// Create a "Terminate Task" message
		ClusterMessage_AppTerminateTask msg = new ClusterMessage_AppTerminateTask();
		msg.setTaskID(TaskID);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"Terminate Task\" message to the cluster." +
					" TaskID : " + TaskID);
			throw e;
		}
	}

//	------------------------------------------------------------------------	
	
	public void Close() {
	
		SA.AddLogEntry("CMM", "Info - Received a \"Close\" instruction.");
		
		// Close Proxy Server
		if(ProxyMachineID.equals(MachineDBLocalID)){
			try {
				Proxy.Close();
			} catch (IOException e1) {
				// TODO remove StackTrace
				e1.printStackTrace();
				SA.AddLogEntry("CMM", "Error - Failed to close Proxy Server.");
			}
		}
		
		// Close Mentor Threads
		// For each mentor in the mentors list
		while(!MentorList.isEmpty()){
			// Remove the first mentor in the list
			Mentor mentor = MentorList.removeFirst();
			// Close the mentor
			// note: The mentor may be finished, and the thread may be long dead
			mentor.Close();
		}
		
		// Close Spread Connection
		try {	
			// Remove listener from the connection
			ClusterConnection.remove(SpreadListener);
			// Leave the Cluster group
			ClusterGroup.leave();
			// Disconnect from the Spread connection
			ClusterConnection.disconnect();
			SA.AddLogEntry("CMM", "Info - Disconnected Spread connection to the Cluster.");
			
			// TODO Add CMM tread termination
			
		} catch (SpreadException e) {
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to close the Spread connection to the Cluster.");
		}
	}
	
//	------------------------------------------------------------------------
//  AdvancedSpreadListener Interface	
//	------------------------------------------------------------------------	

	public synchronized void ReceiveLocalFirstJoin() {
		
		SA.AddLogEntry("CMM", "Info - Received self joining to cluster message.");
		SA.AddLogEntry("CMM", "Info - This machine is the first in the Cluster - skipping training.");
		
		// Wake the constructor thread
		ConstructorThread.interrupt();
	}

//	------------------------------------------------------------------------	
	
	public void ReceiveLocalClusterJoin() {
		
		SA.AddLogEntry("CMM", "Info - Received self joining to cluster message. Waiting for a training session.");
		
//		LocalMachineEntry.putMachineState(MachineState.TRAINING);
//		SpreadListener.setLocalMachineState(LocalMachineEntry.getMachineState());
	}
	
//	------------------------------------------------------------------------	
	
	public void ReceiveNewMachineJoin(String MachineName) {
		
		// Hote: this message is NOT pushed into the TrainingMessageQueue
		//       for training machines		
		if(LocalMachineEntry.getMachineState()==MachineState.ACTIVE){
			SA.AddLogEntry("CMM", "Info - Received a \"Membership Join\" message from the cluster. Machine " + MachineName + " has joined the cluster.");

			// TODO ADD a proper decision of who is mentoring the new machine
			if(!MachineDBLocalID.equals(new Integer(0)))
				return;
		
			// Create a snapshot Machine database
			TreeMap<Integer, MachineEntry> SnapMachineDB = new TreeMap<Integer, MachineEntry>();
			// Create a snapshot Task database
			TreeMap<Integer, TaskEntry> SnapTaskDB = new TreeMap<Integer, TaskEntry>();
			synchronized(this){
				// Convert MachineDB from TreeMap into a Set
				Set<Map.Entry<Integer, MachineEntry>> MachineSet = MachineDB.entrySet(); 
				Iterator<Map.Entry<Integer, MachineEntry>> it = MachineSet.iterator();
				// for each element in the set
				while(it.hasNext()){
					// retrieve the set element
					Map.Entry<Integer, MachineEntry> MapEntry = it.next();
					// Create a copy of the Machine ID
					Integer SnapKey = new Integer(MapEntry.getKey());
					// Create a copy of the MachineEntry
					MachineEntry SnapEntry = null;
					try {
						SnapEntry = MapEntry.getValue().clone();
					} catch (CloneNotSupportedException e) {
						// TODO remove stack print
						e.printStackTrace();
						SA.AddLogEntry("CMM", "Error - Failed to create a Snapshot Machine database.");
						SA.UnrecoverableClusterError();
						return;
					}
				
					// put the new copy into the snapshot machine database
					SnapMachineDB.put(SnapKey, SnapEntry);
				}
			
				// Convert TaskDB from TreeMap into a Set
				Set<Map.Entry<Integer, TaskEntry>> TaskSet = TaskDB.entrySet();
				Iterator<Map.Entry<Integer, TaskEntry>> Tit = TaskSet.iterator();
				while(Tit.hasNext()){
					Map.Entry<Integer, TaskEntry> TMapEntry = Tit.next();
					Integer SnapKey = new Integer(TMapEntry.getKey());
					TaskEntry SnapEntry = null;
					try {
						SnapEntry = TMapEntry.getValue().clone();
					} catch (CloneNotSupportedException e) {
						// TODO remove stack print
						e.printStackTrace();
						SA.AddLogEntry("CMM", "Error - Failed to create a Snapshot Task database.");
						SA.UnrecoverableClusterError();
						return;
					}
					// put the new copy into the snapshot task database
					SnapTaskDB.put(SnapKey, SnapEntry);	
				}
			}
		
			if(InteractiveTrainingMode)
				WaitForEnter("Press Enter to send \"Training New Task\" message to " + MachineName);
			
			// Start mentoring the new machine
			SA.AddLogEntry("CMM", "Info - Started mentoring new machine " + MachineName);
			// Create a "Training New Task" message
			MachineTrainingData data = new MachineTrainingData();
			data.setMachineName(MachineName);
			data.setMachineDB(SnapMachineDB);
			ClusterMessage_TrainingNewTask msg = new ClusterMessage_TrainingNewTask();
			msg.setContext(data);
			try {
				// Send the message to the Cluster
				SendClusterMessage(msg);
			} catch (CMMExn_ClusterErr e) {
				// TODO remove stack trace
				e.printStackTrace();
				SA.AddLogEntry("CMM", "Error - Failed to send a \"Training New Task\" message.");
				SA.UnrecoverableClusterError();
				return;
			}
		
			// Put Snapshot TaskDatabase into the Snapshot TaskDB List
			ToBeMentoredList.addLast(new ToBeMentoredEntry(MachineName, SnapTaskDB));
		}
	}
	
//	------------------------------------------------------------------------
	
	public synchronized void ReceiveClusterLeave(String LeavingMachineName) {
		
		SA.AddLogEntry("CMM", "Info - Received a \"Membership Leave\" message from the Cluster. Machine " + LeavingMachineName + " has left the Cluster.");
		
		if(LocalMachineEntry.getMachineState()==MachineState.PRE_TRAINING){
			return;
		}
	
		Integer LeaverMachineID = null;
		// Convert MachineDB from TreeMap into a Set
		Set<Map.Entry<Integer, MachineEntry>> MachineSet = MachineDB.entrySet();
		Iterator<Map.Entry<Integer, MachineEntry>> it = MachineSet.iterator();
		// for each element in the set
		while(it.hasNext()){
			// retrieve the set element
			Map.Entry<Integer, MachineEntry> MapEntry = it.next();
			// If machine name matches the leaving machine
			if(MapEntry.getValue().getClusterMachineName().equals(LeavingMachineName)) {
				LeaverMachineID = MapEntry.getKey();
				break;
			}
		}
		if(LeaverMachineID != null){
			// If in TRAINING state
			if(LocalMachineEntry.getMachineState()==MachineState.TRAINING){
				// Create a dummy "Remove Machine" message
				ClusterMessage_RemoveMacine Rm_Msg = new ClusterMessage_RemoveMacine();
				Rm_Msg.setSenderID(LeaverMachineID);
				// Put the message in the Training Message Queue
				StoreMessageWhileTraining(Rm_Msg);
				return;
			}
			
			// remove the machine from the Machine Database
			RemoveMachineFromCluster(LeaverMachineID);
		}
		else {
			SA.AddLogEntry("CMM", "Error - No Machine was found with the machine name - " + LeavingMachineName + " in the Machine Database.");
			SA.UnrecoverableClusterError();
			return;
		}
		
		// TODO: Add Fault Tolerance here!!
		
		return;
	}
	
//	------------------------------------------------------------------------	
//	------------------------------------------------------------------------
	
	public synchronized void ReceiveAppNewTask(Integer MachineID, SATaskContext Context) {
	
		SA.AddLogEntry("CMM", "Info - Recieved an \"Application New Task\" message from the cluster." +
				" Sender ID - " + MachineID);
		
		Integer NewTaskID = null;
		try {
			NewTaskID = InsertNewTaskIntoDB(MachineID, Context);
		} catch (CMMExn_TaskErr e1) {
			SA.AddLogEntry("CMM", "Error - Error in Cluster message - dropping the message.");
			// If message originated on this machine
			if(MachineID.equals(MachineDBLocalID))
				SA.DropNewTask();
			return;
		}
		SA.AddLogEntry("CMM", "Info - Added the new application task to the Task Database with TaskID=" + NewTaskID);
		
		// If local machine is not active - the message could not have originated from it.
		if(LocalMachineEntry.getMachineState()!=MachineState.ACTIVE)
			return;
		
		// If message originated on this machine
		if(MachineID.equals(MachineDBLocalID))
			try {
				// Report the New Task ID to the SA
				SA.ReceiveNewTaskId(NewTaskID);
			} catch (Exception e) {
				SA.AddLogEntry("CMM", "Error - SA did not expect to recieve New Task ID. Removing task from Database");
				e.printStackTrace();
				try {
					// Send "Application Task Finish" message to the Cluster
					AppTaskFinish(NewTaskID);
				} catch (Exception e1) {
					// TODO remove stack print
					e1.printStackTrace();
					SA.AddLogEntry("CMM", "Error - Could not send \"Application Task Finish\" message to the Cluster.");
				}
				SA.UnrecoverableClusterError();
			}
			LocalMachineEntry.incTaskCount();
	}

//	------------------------------------------------------------------------	
	
	public synchronized void ReceiveAppUpdateTask(Integer MachineID, Integer TaskID, SAIncramentalTaskContext ContextUpdate) {

		SA.AddLogEntry("CMM", "Info - Recieved an \"Application Update Task\" message from the cluster." +
				" Sender ID - " + MachineID + " , TaskID - " + TaskID);
		
		if(!MachineDB.containsKey(MachineID)){
			SA.AddLogEntry("CMM", "Error - Sender MachineID " + MachineID + " does not exist in the Machine Database.");
			SA.UnrecoverableClusterError();
			return;
		}
		if(!TaskDB.containsKey(TaskID)){
			SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
			SA.UnrecoverableClusterError();
			return;
		}
			
		// Retrieve task entry from task database
		TaskEntry TEntry = TaskDB.get(TaskID);
		// Verify that task belongs to the correct machine
		if(!TEntry.getAssignedServerID().equals(MachineID)){
			SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + TEntry.getAssignedServerID() 
					+ ") does not match sender MachineID (" + MachineID +")");
			SA.UnrecoverableClusterError();
			return;
		}
		// Retrieve task context from task entry
		TaskContext Context = TEntry.getContext();
		// Verify that task context type is APPLICATION
		if(Context.getType()!=TaskType.APPLICATION){
			SA.AddLogEntry("CMM", "Error - Task type is " + 
					Context.getType() + " instead of APPLICATION.");
			SA.UnrecoverableClusterError();
			return;
		}
		// Convert to SA task context
		SATaskContext OldSAContext = (SATaskContext)Context;
		// Reconstruct task's context from old context and incramental change
		SATaskContext NewSAContext = SA.ReconstructTaskContext(OldSAContext, ContextUpdate);
		// Put reconstructed context into the task's entry
		TEntry.putContext(NewSAContext);
	}
		
//	------------------------------------------------------------------------	
	
	public synchronized void ReceiveAppTerminateTask(Integer MachineID, Integer TaskID) {

		SA.AddLogEntry("CMM", "Info - Recieved a \"Application Terminate Task\" message from the cluster." +
				" Sender ID - " + MachineID + " , TaskID - " + TaskID);

		if(!MachineDB.containsKey(MachineID)){
			SA.AddLogEntry("CMM", "Error - Sender MachineID " + MachineID + " does not exist in the Machine Database.");
			SA.UnrecoverableClusterError();
			return;
		}
		if(!TaskDB.containsKey(TaskID)){
			SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
			SA.UnrecoverableClusterError();
			return;
		}
			
		// Retrieve task's entry from the task database
		TaskEntry TEntry = TaskDB.get(TaskID);
		// Verify that the task belongs to the correct machine
		if(!TEntry.getAssignedServerID().equals(MachineID)){
			SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + TEntry.getAssignedServerID() 
					+ ") does not match sender MachineID (" + MachineID +")");
			SA.UnrecoverableClusterError();
			return;
		}
		// Remove task's entry from the task database
		TaskDB.remove(TaskID);
		// Retrieve machine's entry from the machine database
		MachineEntry MEntry = MachineDB.get(MachineID);
		// Decrament the task count of the machine
		MEntry.decTaskCount();
		
		if(LocalMachineEntry.getMachineState()==MachineState.ACTIVE){
			// If the task was on this machine
			if(MachineDBLocalID.equals(MachineID)) {
				LocalMachineEntry.decTaskCount();
			}
		}
	}
	
//	------------------------------------------------------------------------	

	public synchronized void ReceiveNewServerReady(MachineEntry newMachineEntry) {

		SA.AddLogEntry("CMM", "Info - Recieved a \"New Server Ready\" message from the cluster." +
				" Sender Name - " + newMachineEntry.getClusterMachineName());
		
		// If the local machine is in PRE_TRAINING state - do not insert the 
		// new MachineEntry to the Machine database
		if(LocalMachineEntry.getMachineState() == MachineState.PRE_TRAINING)
			return;
		
		// Get max MachineID from the machine database
		Integer MaxMachineID;
		if(MachineDB.isEmpty())
			MaxMachineID = new Integer(-1);
		else
			MaxMachineID = MachineDB.lastKey();
		
		// New machine ID is max+1 
		Integer NewMachineID = new Integer(MaxMachineID.intValue() + 1);
		
		SA.AddLogEntry("CMM", "Info - The new MachineID allocated for the new machine " + newMachineEntry.getClusterMachineName() + " is " + NewMachineID);
		
		// Put the new machine entry into the machine database
		MachineDB.put(NewMachineID, newMachineEntry);
		
		// If this message originated on this machine
		if(newMachineEntry.getClusterMachineName().equals(LocalMachineEntry.getClusterMachineName())) {
			// Update the local MachineID
			MachineDBLocalID = NewMachineID;
			// Wake the constructor thread
			ConstructorThread.interrupt();
		}
		
	}

//	------------------------------------------------------------------------	
	
	public void ReceiveUnsupportedMessage() {
		SA.AddLogEntry("CMM", "Error - Received an unsupported message from the cluster.");
		SA.UnrecoverableClusterError();
	}

//	------------------------------------------------------------------------	
	
	public synchronized void ReceiveProxyNewTask(Integer MachineID, ProxyContext Context) {
		
		SA.AddLogEntry("CMM", "Info - Recieved a \"Proxy New Task\" message from the cluster." +
				" Sender ID - " + MachineID);
		
		Integer NewTaskID = null;
		try {
			NewTaskID = InsertNewTaskIntoDB(MachineID, Context);
		} catch (CMMExn_TaskErr e1) {
			SA.AddLogEntry("CMM", "Error - Error in Cluster message - dropping the message.");
			return;
		}
		
		SA.AddLogEntry("CMM", "Info - Added a new Proxy Server task with TaskID=" + NewTaskID);
		
		if(ProxyMachineID != null){
			SA.AddLogEntry("CMM", "Error - Recieved a \"Proxy New Task\" message while Proxy Server is already active.");
			SA.UnrecoverableClusterError();
		}
		else
			ProxyMachineID = MachineID;
		
		LocalMachineEntry.incTaskCount();
	}
	
//	------------------------------------------------------------------------
	
	public void ReceiveTrainingNewTask(Integer MachineID, MachineTrainingData Data){

		SA.AddLogEntry("CMM", "Info - Received a \"Training New Task\" message from the cluster." +
				" SenderID=" + MachineID + " for Trainee machine name - " + Data.getMachineName());
		
		// If local machine is in PRE_TRAINING state
		if(LocalMachineEntry.getMachineState() == MachineState.PRE_TRAINING) {
			// Check if the message is for this local machine
			if(LocalMachineEntry.getClusterMachineName().equals(Data.getMachineName())){
				SA.AddLogEntry("CMM", "Info - Beginning the Training session.");
				MachineDB = Data.getMachineDB();
				SA.AddLogEntry("CMM", "Info - Learned Machine database from Training session.");
				TrainingMessageQueue = new MessageQueue();
				
				// Move to TRAINING state
				LocalMachineEntry.putMachineState(MachineState.TRAINING);
				SpreadListener.setLocalMachineState(LocalMachineEntry.getMachineState());
			}
		}
		// If local machine is in ACTIVE state
		// note: TRAINING stage is handled in the listener
		else {
			// Create a new training context
			TrainingContext Context = new TrainingContext();
			Context.setLastTrainedTaskID(null);
			Context.setTraineeName(Data.getMachineName());
			Integer TrainingTaskID = null;
			synchronized(this){
				try {
					// Add the new Training task into the Task database
					TrainingTaskID = InsertNewTaskIntoDB(MachineID, Context);
				} catch (CMMExn_TaskErr e) {
					// TODO remove stack trace
					e.printStackTrace();
					SA.AddLogEntry("CMM", "Error - Failed to add the new training task into the Task database.");
					SA.UnrecoverableClusterError();
					return;
				}
			}
			SA.AddLogEntry("CMM", "Info - Added a Training task to the task database. TaskID=" + TrainingTaskID);

			// If local machine was the one who sent the message
			if(MachineID.equals(MachineDBLocalID)){
				// This training task is handled by this machine
				
				// Retrieve the Training entry for this task
				ToBeMentoredEntry FirstEntry = ToBeMentoredList.getFirst();
				if(!FirstEntry.getMachineName().equals(Data.getMachineName())) {
					// Message is not consistent with ToBeMentored Queue
					SA.AddLogEntry("CMM", "Error - Got unexpected Trainee machine name in the \"Training New Task\" message");
					SA.UnrecoverableClusterError();
					return;
				}
				
				LocalMachineEntry.incTaskCount();
				
				// Remove the training data from the ToBeMentoredList
				ToBeMentoredList.removeFirst();
				// Create a new Mentor and add it to the Mentor List
				MentorList.addLast(new Mentor(this, TrainingTaskID, Data.getMachineName(), FirstEntry.getSnapshotTaskDB(), InteractiveTrainingMode));
				
				SA.AddLogEntry("CMM", "Info - Started a new Mentor thread for this mentoring session.");
			}
		}
	}

//	------------------------------------------------------------------------	
	
	public synchronized void ReceiveTrainingUpdateTask(Integer MachineID, Integer TrainingSessionID, TaskTrainingData Data, ClusterMessage ClusterMSG) {
		
		SA.AddLogEntry("CMM", "Info - Received a \"Training Update Task\" message from the cluster." +
				" SenderID=" + MachineID + " for Trainee \"" + Data.getTraineeName() + "\", TaskID " + Data.getTrainedTaskID());
		
		// If local machine is in PRE_TRAINING state
		if(LocalMachineEntry.getMachineState() == MachineState.PRE_TRAINING) {
			// Ignore
		}
		// If local machine is in TRAINING state
		else if(LocalMachineEntry.getMachineState() == MachineState.TRAINING) {
			// If the message is addressed to this local machine
			if(LocalMachineEntry.getClusterMachineName().equals(Data.getTraineeName())){
				// Put the transfered Task entry into the Task database
				TaskDB.put(Data.getTrainedTaskID(), Data.getTrainedTaskEntry());
				
				// If this task type is PROXY
				if(Data.getTrainedTaskEntry().getContext().getType()==TaskType.PROXY){
					// Store the task's MachineID
					ProxyMachineID = Data.getTrainedTaskEntry().getAssignedServerID();
					SA.AddLogEntry("CMM", "Info - Learned that Proxy Server is currently at MachineID " + ProxyMachineID);
				}
				
				SA.AddLogEntry("CMM", "Info - Learned Task " + Data.getTrainedTaskID() + " from the Training session.");
			}
			// If the message is not adressed to this local machine
			else {
				// Store the message into the Message Queue to process it later
				StoreMessageWhileTraining(ClusterMSG);
			}
		}
		// If local machine is in ACTIVE state
		else {
			// Check if Training session exists in the Task database
			if(!TaskDB.containsKey(TrainingSessionID)) {
				SA.AddLogEntry("CMM", "Error - Training session TaskID " + TrainingSessionID + " does not exist in the Task database.");
				SA.UnrecoverableClusterError();
				return;
			}
			
			// Retrieve the training session task context from the task database
			TaskContext Context = TaskDB.get(TrainingSessionID).getContext();
			// check that the task context is of a TRAINING type
			if(Context.getType() != TaskType.TRAINING) {
				SA.AddLogEntry("CMM", "Error - Training session TaskID " + TrainingSessionID + " does not correspond with a Training task context.");
				SA.UnrecoverableClusterError();
				return;
			}
			// Check that the machine that sent this message is the assigned Mentor for this training session.
			if(!TaskDB.get(TrainingSessionID).getAssignedServerID().equals(MachineID)){
				SA.AddLogEntry("CMM", "Error - Sender MachineID (" + MachineID + ") does not match the session's Mentor MachineID " + TaskDB.get(TrainingSessionID).getAssignedServerID());
				SA.UnrecoverableClusterError();
				return;
			}
			
			
			// Update the "Last Trained TaskID" of the Training context to the TaskID of this message 
			((TrainingContext)Context).setLastTrainedTaskID(Data.getTrainedTaskID());
		}
		
	}
	
//	------------------------------------------------------------------------	
	
	public synchronized void ReceiveTrainingTerminateTask(Integer MachineID, Integer TrainingSessionID, String TraineeName, ClusterMessage ClusterMSG) {

		SA.AddLogEntry("CMM", "Info - Received a \"Training Terminate Task\" message from the cluster." +
				" SenderID=" + MachineID + " for Trainee \"" + TraineeName + "\".");
		
		// If local machine is in TRAINING
		if(LocalMachineEntry.getMachineState() == MachineState.TRAINING) {
			// If this message is addressed to this local machine
			if(LocalMachineEntry.getClusterMachineName().equals(TraineeName)){
				SA.AddLogEntry("CMM", "Info - Finished the Training session.");
				// Process all the non training messages that came 
				// during the training session 
				ProcessTaskQueue();
				SA.AddLogEntry("CMM", "Info - Updated the Task database according to the Training Task queue.");
				// Wake the Constructor
				ConstructorThread.interrupt();
			}
			// If this message is not addressed to this local machine
			else{
				// Store message in the Message Queue to proccess it later 
				StoreMessageWhileTraining(ClusterMSG);
			}
		}
		// If local machine is in ACTIVE
		else if(LocalMachineEntry.getMachineState() == MachineState.ACTIVE) {
			// Check if Training session exists in the Task database
			if(!TaskDB.containsKey(TrainingSessionID)){
				SA.AddLogEntry("CMM", "Error - Training session TaskID " + TrainingSessionID + " does not exist in the Task database.");
				SA.UnrecoverableClusterError();
				return;
			}
			// Retrieve the training session task context from the task database
			TaskContext Context = TaskDB.get(TrainingSessionID).getContext();
			// check that the task context is of a TRAINING type
			if(Context.getType() != TaskType.TRAINING) {
				SA.AddLogEntry("CMM", "Error - Training session TaskID " + TrainingSessionID + " does not correspond with a Training task context.");
				SA.UnrecoverableClusterError();
				return;
			}
			// Check that the machine that sent this message is the assigned Mentor for this training session.
			if(!TaskDB.get(TrainingSessionID).getAssignedServerID().equals(MachineID)){
				SA.AddLogEntry("CMM", "Error - Sender MachineID (" + MachineID + ") does not match the session's Mentor MachineID " + TaskDB.get(TrainingSessionID).getAssignedServerID());
				SA.UnrecoverableClusterError();
				return;
			}
			
			// Remove the Training session from the Task database
			TaskDB.remove(TrainingSessionID);
			// Decrease the task count of the ex-Mentor machine 
			MachineDB.get(MachineID).decTaskCount();
			
			if(MachineDBLocalID.equals(MachineID))
				LocalMachineEntry.decTaskCount();
		}
	}

	public void StoreMessageWhileTraining(ClusterMessage message) {
		// Push the incoming message into the Training message queue 
		// for later processing.
		TrainingMessageQueue.Push(message);
	}
}
